#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
机器人关节日志可视化工具
左侧显示日志文件列表及时间范围
右侧可视化选中的日志数据
@author- chenqibang
@date- 2025-11-14
"""

import re
import datetime
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from matplotlib.widgets import SpanSelector
import matplotlib.dates as mdates
import threading
import numpy as np

plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['agg.path.chunksize'] = 10000  # optimization for large data plotting

class MotorLogViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("关节模组日志可视化")
        self.root.geometry("1800x1200")
        
        self.current_directory = ""
        self.log_files_info = {}
        self.selected_state_files = []
        self.selected_cmd_files = []
        
        self.state_data = None
        self.cmd_data = None
        self.current_leg = 0
        self.current_motor = 0
        self.view_mode = 'leg'  # 'leg' or 'motor'
        
        # performance optimization parameters
        self.max_display_points = 5000  # maximum number of points displayed for each curve
        self.enable_downsampling = True  # enable downsampling
        
        # time range selection
        self.time_range_start = None
        self.time_range_end = None
        self.full_time_range = None
        self.span_selector = None
        
        # 点云模式
        self.point_cloud_mode = False
        self.point_cloud_data = None  # 存储点云数据用于导出
        
        self.setup_ui()
        
    def setup_ui(self):
        # main container - use Frame instead of PanedWindow to implement fixed width
        main_container = ttk.Frame(self.root)
        main_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # left panel - fixed width pixels
        left_frame = ttk.Frame(main_container, width=400)
        left_frame.pack(side=tk.LEFT, fill=tk.Y)
        left_frame.pack_propagate(False)  # prevent sub-components from changing the frame size
        
        # separator
        separator = ttk.Separator(main_container, orient=tk.VERTICAL)
        separator.pack(side=tk.LEFT, fill=tk.Y, padx=2)
        
        # right panel
        right_frame = ttk.Frame(main_container)
        right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # folder selection
        folder_frame = ttk.Frame(left_frame)
        folder_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Label(folder_frame, text="目标文件夹:").pack(side=tk.TOP, anchor=tk.W)
        
        folder_select_frame = ttk.Frame(folder_frame)
        folder_select_frame.pack(fill=tk.X, pady=5)
        
        self.folder_label = ttk.Label(folder_select_frame, text="未选择", foreground="gray", relief=tk.SUNKEN)
        self.folder_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
        ttk.Button(folder_select_frame, text="浏览", command=self.select_folder).pack(side=tk.LEFT, padx=5)
        ttk.Button(folder_select_frame, text="刷新", command=self.refresh_files).pack(side=tk.LEFT, padx=5)
        
        # file list
        list_frame = ttk.Frame(left_frame)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # unified log list
        ttk.Label(list_frame, text="日志文件 (可多选State和Cmd):", font=("", 10, "bold")).pack(anchor=tk.W)
        
        log_list_frame = ttk.Frame(list_frame)
        log_list_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
        
        log_scroll = ttk.Scrollbar(log_list_frame)
        log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.log_listbox = tk.Listbox(log_list_frame, yscrollcommand=log_scroll.set, 
                                      selectmode=tk.MULTIPLE, font=("Consolas", 8))
        self.log_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        log_scroll.config(command=self.log_listbox.yview)
        self.log_listbox.bind('<<ListboxSelect>>', self.on_file_select)
        
        # load button
        load_frame = ttk.Frame(left_frame)
        load_frame.pack(fill=tk.X, padx=5, pady=5)
        ttk.Button(load_frame, text="加载并可视化", command=self.load_and_visualize, 
                  style="Accent.TButton").pack(fill=tk.X)
        
        # current selection display
        ttk.Label(left_frame, text="当前选择:", font=("", 10, "bold")).pack(anchor=tk.W, padx=5, pady=(5,0))
        selection_frame = ttk.Frame(left_frame)
        selection_frame.pack(fill=tk.X, padx=5, pady=(0,5))
        
        self.selection_label = ttk.Label(selection_frame, text="未选择文件", 
                                         foreground="gray", wraplength=340, justify=tk.LEFT)
        self.selection_label.pack(anchor=tk.W)
        
        # state information
        ttk.Label(left_frame, text="日志信息:", font=("", 10, "bold")).pack(anchor=tk.W, padx=5)
        info_frame = ttk.Frame(left_frame)
        info_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        info_scroll = ttk.Scrollbar(info_frame)
        info_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.info_text = tk.Text(info_frame, height=10, wrap=tk.WORD, yscrollcommand=info_scroll.set, 
                                font=("Consolas", 9))
        self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        info_scroll.config(command=self.info_text.yview)
        
        # control bar (right panel is defined above)
        control_frame = ttk.Frame(right_frame)
        control_frame.pack(fill=tk.X, padx=5, pady=5)
        
        # view mode selection
        view_mode_frame = ttk.LabelFrame(control_frame, text="视图模式", padding=5)
        view_mode_frame.pack(side=tk.LEFT, padx=5)
        
        self.view_mode_var = tk.StringVar(value='leg')
        ttk.Radiobutton(view_mode_frame, text="按腿部", variable=self.view_mode_var, 
                       value='leg', command=self.switch_view_mode).pack(side=tk.LEFT, padx=3)
        ttk.Radiobutton(view_mode_frame, text="按电机", variable=self.view_mode_var, 
                       value='motor', command=self.switch_view_mode).pack(side=tk.LEFT, padx=3)
        
        # selection container - fixed position, content dynamic switch
        selection_container = ttk.Frame(control_frame)
        selection_container.pack(side=tk.LEFT, padx=5)
        
        # leg selection box
        self.leg_frame = ttk.LabelFrame(selection_container, text="选择腿部", padding=5)
        self.leg_frame.grid(row=0, column=0, sticky='nsew')
        
        self.leg_var = tk.IntVar(value=0)
        for i in range(4):
            ttk.Radiobutton(self.leg_frame, text=f"Leg {i}", variable=self.leg_var, 
                           value=i, command=self.update_plot).pack(side=tk.LEFT, padx=3)
        
        # motor selection box
        self.motor_frame = ttk.LabelFrame(selection_container, text="选择电机", padding=5)
        self.motor_frame.grid(row=0, column=0, sticky='nsew')
        
        self.motor_var = tk.IntVar(value=0)
        for i in range(4):
            ttk.Radiobutton(self.motor_frame, text=f"Motor {i+1}", variable=self.motor_var, 
                           value=i, command=self.update_plot).pack(side=tk.LEFT, padx=3)
        
        # default display leg selection, hide motor selection
        self.motor_frame.grid_remove()
        
        # other buttons
        button_frame = ttk.Frame(control_frame)
        button_frame.pack(side=tk.LEFT, padx=10)
        
        ttk.Button(button_frame, text="刷新图表", command=self.update_plot).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="统计信息", command=self.show_statistics).pack(side=tk.LEFT, padx=5)
        
        # performance optimization switch
        self.downsample_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(button_frame, text="智能采样", variable=self.downsample_var, 
                       command=self.update_plot).pack(side=tk.LEFT, padx=5)
        
        # 点云模式开关
        self.point_cloud_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(button_frame, text="点云", variable=self.point_cloud_var, 
                       command=self.toggle_point_cloud_mode).pack(side=tk.LEFT, padx=5)
        
        # 导出点云数据按钮
        ttk.Button(button_frame, text="导出点云", command=self.export_point_cloud).pack(side=tk.LEFT, padx=5)
        
        # chart area (including time axis selector)
        self.plot_container = ttk.Frame(right_frame)
        self.plot_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        self.fig = None
        self.canvas = None
        self.toolbar = None
        self.time_selector_ax = None
        
    def select_folder(self):
        folder = filedialog.askdirectory(title="选择日志文件夹", initialdir=self.current_directory or ".")
        if folder:
            self.current_directory = folder
            self.folder_label.config(text=folder, foreground="black")
            self.refresh_files()
    
    def refresh_files(self):
        if not self.current_directory or not os.path.exists(self.current_directory):
            messagebox.showwarning("警告", "请先选择有效的文件夹")
            return
        
        self.log_listbox.delete(0, tk.END)
        self.log_files_info.clear()
        self.info_text.delete(1.0, tk.END)
        
        self.info_text.insert(tk.END, "正在扫描日志文件...\n")
        self.root.update()
        
        # find all log files
        log_files = [f for f in os.listdir(self.current_directory) if f.endswith('.log')]
        
        state_files = sorted([f for f in log_files if 'state' in f.lower()])
        cmd_files = sorted([f for f in log_files if 'cmd' in f.lower()])
        
        self.info_text.insert(tk.END, f"找到 {len(state_files)} 个State日志, {len(cmd_files)} 个Cmd日志\n")
        self.info_text.insert(tk.END, "正在分析文件时间范围...\n")
        self.root.update()
        
        # analyze each file
        all_files = []
        for filename in state_files + cmd_files:
            filepath = os.path.join(self.current_directory, filename)
            self.info_text.insert(tk.END, f"分析 {filename}...\n")
            self.root.update()
            
            time_range = self.get_file_time_range(filepath)
            if time_range:
                file_type = 'state' if 'state' in filename.lower() else 'cmd'
                self.log_files_info[filename] = {
                    'path': filepath,
                    'type': file_type,
                    'time_range': time_range
                }
                all_files.append((filename, file_type, time_range))
        
        # sort by file name, State first, Cmd later
        all_files.sort(key=lambda x: (0 if x[1] == 'state' else 1, x[0]))
        
        # fill unified list
        for filename, file_type, time_range in all_files:
            start_time, end_time, count = time_range
            type_label = "[State]" if file_type == 'state' else "[Cmd] "
            display_text = f"{type_label} {filename} | {start_time.strftime('%H:%M:%S')} - {end_time.strftime('%H:%M:%S')} | {count}条"
            self.log_listbox.insert(tk.END, display_text)
        
        self.info_text.insert(tk.END, "\n扫描完成！\n")
        self.info_text.see(tk.END)
    
    def get_file_time_range(self, filepath):
        """快速获取文件时间范围"""
        try:
            timestamps = []
            with open(filepath, 'r', encoding='utf-8') as f:
                lines = f.readlines()
                count = 0
                for line in lines:
                    timestamp = self.parse_timestamp(line)
                    if timestamp:
                        timestamps.append(timestamp)
                        count += 1
            
            if timestamps:
                return (timestamps[0], timestamps[-1], count)
        except Exception as e:
            self.info_text.insert(tk.END, f"分析文件 {os.path.basename(filepath)} 出错: {e}\n")
        return None
    
    def parse_timestamp(self, line):
        timestamp_match = re.match(r'\[(\d{4}/\d{2}/\d{2} \d{2}\.\d{2}\.\d{2}\.\d{3})\]', line)
        if timestamp_match:
            timestamp_str = timestamp_match.group(1).replace('.', ':')
            try:
                return datetime.datetime.strptime(timestamp_str, '%Y/%m/%d %H:%M:%S:%f')
            except:
                try:
                    return datetime.datetime.strptime(timestamp_str[:-4], '%Y/%m/%d %H:%M:%S')
                except:
                    pass
        return None
    
    def on_file_select(self, event):
        """处理文件选择事件"""
        selections = self.log_listbox.curselection()
        self.selected_state_files = []
        self.selected_cmd_files = []
        
        for idx in selections:
            selected_text = self.log_listbox.get(idx)
            # extract file name (remove type label)
            parts = selected_text.split(' | ')
            filename_with_type = parts[0]
            if filename_with_type.startswith('[State]'):
                filename = filename_with_type.replace('[State]', '').strip()
                self.selected_state_files.append(filename)
            elif filename_with_type.startswith('[Cmd]'):
                filename = filename_with_type.replace('[Cmd]', '').strip()
                self.selected_cmd_files.append(filename)
        
        self.update_selection_display()
    
    def switch_view_mode(self):
        """切换视图模式"""
        mode = self.view_mode_var.get()
        if mode == 'leg':
            # display leg selection, hide motor selection
            self.leg_frame.grid(row=0, column=0, sticky='nsew')
            self.motor_frame.grid_remove()
        else:
            # display motor selection, hide leg selection
            self.motor_frame.grid(row=0, column=0, sticky='nsew')
            self.leg_frame.grid_remove()
        
        self.update_plot()
    
    def toggle_point_cloud_mode(self):
        """切换点云模式"""
        self.point_cloud_mode = self.point_cloud_var.get()
        
        if not self.state_data or not self.state_data['timestamps']:
            messagebox.showwarning("警告", "请先加载数据")
            self.point_cloud_var.set(False)
            self.point_cloud_mode = False
            return
        
        mode_text = "点云模式" if self.point_cloud_mode else "常规模式"
        self.info_text.insert(tk.END, f"\n切换到 {mode_text}\n")
        self.info_text.see(tk.END)
        
        self.update_plot()
    
    def collect_point_cloud_data(self):
        """收集所有关节的 torque vs speed 点云数据"""
        if not self.state_data or not self.state_data['timestamps']:
            return None
        
        state_timestamps = self.state_data['timestamps']
        
        # 应用时间范围过滤
        filtered_timestamps = state_timestamps
        if self.time_range_start and self.time_range_end:
            filtered_timestamps = [t for t in state_timestamps 
                                   if self.time_range_start <= t <= self.time_range_end]
        
        # 收集所有腿所有电机的数据
        point_cloud_data = {
            'timestamps': [],
            'legs': []
        }
        
        for leg_id in range(4):
            leg_data = {
                'leg_id': leg_id,
                'motors': []
            }
            
            for motor_id in range(4):
                state_motor = self.state_data['legs'][leg_id]['motors'][motor_id]
                
                # 过滤时间范围内的数据
                motor_torque = []
                motor_speed = []
                motor_timestamps = []
                
                for i, t in enumerate(state_timestamps):
                    if t in filtered_timestamps:
                        motor_torque.append(state_motor['torque'][i])
                        motor_speed.append(state_motor['speed'][i])
                        motor_timestamps.append(t)
                
                leg_data['motors'].append({
                    'motor_id': motor_id,
                    'torque': motor_torque,
                    'speed': motor_speed,
                    'timestamps': motor_timestamps
                })
            
            point_cloud_data['legs'].append(leg_data)
        
        point_cloud_data['timestamps'] = filtered_timestamps
        self.point_cloud_data = point_cloud_data
        return point_cloud_data
    
    def export_point_cloud(self):
        """导出点云数据为CSV文件"""
        if not self.state_data:
            messagebox.showwarning("警告", "请先加载数据")
            return
        
        # 收集点云数据
        self.info_text.insert(tk.END, "\n正在收集点云数据...\n")
        self.root.update()
        
        pc_data = self.collect_point_cloud_data()
        
        if not pc_data:
            messagebox.showerror("错误", "无法收集点云数据")
            return
        
        # 选择保存路径
        from tkinter import filedialog
        import csv
        
        default_filename = f"point_cloud_data_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        filepath = filedialog.asksaveasfilename(
            title="保存点云数据",
            defaultextension=".csv",
            filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
            initialfile=default_filename
        )
        
        if not filepath:
            return
        
        try:
            with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
                writer = csv.writer(csvfile)
                
                # 写入头部信息
                writer.writerow(['# 四足机器人关节扭矩-速度点云数据'])
                writer.writerow(['# 生成时间:', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')])
                if self.time_range_start and self.time_range_end:
                    writer.writerow(['# 时间范围:', 
                                   self.time_range_start.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
                                   '至',
                                   self.time_range_end.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]])
                writer.writerow([])
                
                # 写入数据头
                writer.writerow(['Timestamp', 'Leg_ID', 'Motor_ID', 'Torque(Nm)', 'Speed(rad/s)'])
                
                # 写入数据
                data_count = 0
                for leg in pc_data['legs']:
                    leg_id = leg['leg_id']
                    for motor in leg['motors']:
                        motor_id = motor['motor_id']
                        for i in range(len(motor['torque'])):
                            timestamp = motor['timestamps'][i].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                            torque = motor['torque'][i]
                            speed = motor['speed'][i]
                            writer.writerow([timestamp, leg_id, motor_id, torque, speed])
                            data_count += 1
                
                self.info_text.insert(tk.END, f"✓ 点云数据已导出: {filepath}\n")
                self.info_text.insert(tk.END, f"  总计 {data_count} 个数据点\n")
                self.info_text.see(tk.END)
                
                messagebox.showinfo("成功", f"点云数据已成功导出到:\n{filepath}\n\n总计 {data_count} 个数据点")
                
        except Exception as e:
            messagebox.showerror("错误", f"导出失败: {e}")
            self.info_text.insert(tk.END, f"❌ 导出失败: {e}\n")
    
    def downsample_data(self, timestamps, values, max_points=None):
        """智能下采样 - 保留数据特征的同时减少点数"""
        if max_points is None:
            max_points = self.max_display_points
        
        n = len(timestamps)
        if n <= max_points or not self.downsample_var.get():
            return timestamps, values
        
        # use uniform interval sampling
        indices = np.linspace(0, n - 1, max_points, dtype=int)
        
        # ensure contains key points (extreme points)
        if n > max_points * 2:
            # find the index of the maximum and minimum values
            values_array = np.array(values)
            # find the extreme values in segments
            segment_size = n // (max_points // 10)
            for i in range(0, n - segment_size, segment_size):
                segment = values_array[i:i+segment_size]
                if len(segment) > 0:
                    local_max_idx = i + np.argmax(segment)
                    local_min_idx = i + np.argmin(segment)
                    indices = np.append(indices, [local_max_idx, local_min_idx])
            
            # remove duplicates and sort
            indices = np.unique(indices)
            indices.sort()
        
        sampled_timestamps = [timestamps[i] for i in indices]
        sampled_values = [values[i] for i in indices]
        
        return sampled_timestamps, sampled_values
    
    def filter_data_by_time_range(self, timestamps, *data_arrays):
        """根据时间范围过滤数据"""
        if self.time_range_start is None or self.time_range_end is None:
            return (timestamps,) + data_arrays
        
        # find the index within the time range
        indices = [i for i, t in enumerate(timestamps) 
                  if self.time_range_start <= t <= self.time_range_end]
        
        if not indices:
            return (timestamps,) + data_arrays
        
        filtered_timestamps = [timestamps[i] for i in indices]
        filtered_arrays = tuple([[arr[i] for i in indices] for arr in data_arrays])
        
        return (filtered_timestamps,) + filtered_arrays
    
    def reset_time_range(self):
        """重置时间范围"""
        self.time_range_start = None
        self.time_range_end = None
        if self.state_data:
            self.update_plot()
    
    def update_selection_display(self):
        """更新选择显示标签"""
        display_text = ""
        
        if self.selected_state_files:
            display_text += f"State ({len(self.selected_state_files)}个): "
            if len(self.selected_state_files) <= 2:
                display_text += ", ".join(self.selected_state_files)
            else:
                display_text += f"{self.selected_state_files[0]}, {self.selected_state_files[1]}... 等"
            display_text += "\n"
        
        if self.selected_cmd_files:
            display_text += f"Cmd ({len(self.selected_cmd_files)}个): "
            if len(self.selected_cmd_files) <= 2:
                display_text += ", ".join(self.selected_cmd_files)
            else:
                display_text += f"{self.selected_cmd_files[0]}, {self.selected_cmd_files[1]}... 等"
        
        if not display_text:
            display_text = "未选择文件"
            self.selection_label.config(text=display_text, foreground="gray")
        else:
            self.selection_label.config(text=display_text.strip(), foreground="black")
    
    def load_and_visualize(self):
        if not self.selected_state_files:
            messagebox.showwarning("警告", "请先选择至少一个State日志文件")
            return
        
        self.info_text.delete(1.0, tk.END)
        self.info_text.insert(tk.END, "=" * 50 + "\n")
        self.info_text.insert(tk.END, f"开始加载数据...\n")
        self.info_text.insert(tk.END, "=" * 50 + "\n\n")
        self.root.update()
        
        # load multiple state data and merge
        self.state_data = None
        for state_file in self.selected_state_files:
            self.info_text.insert(tk.END, f"正在解析: {state_file}\n")
            self.root.update()
            
            state_path = self.log_files_info[state_file]['path']
            state_data = self.parse_state_log(state_path)
            
            if state_data and state_data['timestamps']:
                if self.state_data is None:
                    self.state_data = state_data
                else:
                    self.merge_state_data(self.state_data, state_data)
                self.info_text.insert(tk.END, f"✓ State数据加载成功: {len(state_data['timestamps'])} 条记录\n")
            else:
                self.info_text.insert(tk.END, f"❌ State数据加载失败\n")
            self.root.update()
        
        if not self.state_data or not self.state_data['timestamps']:
            messagebox.showerror("错误", "加载State数据失败或数据为空")
            self.info_text.insert(tk.END, "❌ State数据加载失败\n")
            return
        
        # sort and merge the state data
        self.sort_state_data()
        self.info_text.insert(tk.END, f"\n✓ 总计State数据: {len(self.state_data['timestamps'])} 条记录\n")
        self.info_text.insert(tk.END, f"  时间范围: {self.state_data['timestamps'][0]} 至 {self.state_data['timestamps'][-1]}\n\n")
        
        # set full time range
        self.full_time_range = (self.state_data['timestamps'][0], self.state_data['timestamps'][-1])
        self.time_range_start = self.full_time_range[0]
        self.time_range_end = self.full_time_range[1]
        
        self.root.update()
        
        # load cmd data
        self.cmd_data = None
        if self.selected_cmd_files:
            for cmd_file in self.selected_cmd_files:
                cmd_path = self.log_files_info[cmd_file]['path']
                self.info_text.insert(tk.END, f"正在解析: {cmd_file}\n")
                self.root.update()
                
                cmd_data = self.parse_cmd_log(cmd_path)
                if cmd_data and cmd_data['timestamps']:
                    if self.cmd_data is None:
                        self.cmd_data = cmd_data
                    else:
                        self.merge_cmd_data(self.cmd_data, cmd_data)
                    self.info_text.insert(tk.END, f"✓ Cmd数据加载成功: {len(cmd_data['timestamps'])} 条记录\n")
                else:
                    self.info_text.insert(tk.END, f"❌ Cmd数据加载失败\n")
                self.root.update()
            
            if self.cmd_data:
                # sort and merge the cmd data
                self.sort_cmd_data()
                self.info_text.insert(tk.END, f"\n✓ 总计Cmd数据: {len(self.cmd_data['timestamps'])} 条记录\n")
                self.info_text.insert(tk.END, f"  时间范围: {self.cmd_data['timestamps'][0]} 至 {self.cmd_data['timestamps'][-1]}\n\n")
        
        self.info_text.insert(tk.END, "=" * 50 + "\n")
        self.info_text.insert(tk.END, "数据加载完成！开始绘制图表...\n")
        self.info_text.insert(tk.END, "=" * 50 + "\n")
        self.info_text.see(tk.END)
        self.root.update()
        
        self.update_plot()
    
    def merge_state_data(self, target, source):
        """合并两个state数据"""
        target['timestamps'].extend(source['timestamps'])
        for leg_id in range(4):
            for motor_id in range(4):
                for key in ['temp', 'voltage', 'error', 'position', 'speed', 'torque']:
                    target['legs'][leg_id]['motors'][motor_id][key].extend(
                        source['legs'][leg_id]['motors'][motor_id][key])
    
    def merge_cmd_data(self, target, source):
        """合并两个cmd数据"""
        target['timestamps'].extend(source['timestamps'])
        for leg_id in range(4):
            target['legs'][leg_id]['enable'].extend(source['legs'][leg_id]['enable'])
            for motor_id in range(4):
                for key in ['target_position', 'target_speed', 'target_torque']:
                    target['legs'][leg_id]['motors'][motor_id][key].extend(
                        source['legs'][leg_id]['motors'][motor_id][key])
    
    def sort_state_data(self):
        """按时间戳排序state数据"""
        if not self.state_data or not self.state_data['timestamps']:
            return
        
        # create index list
        indices = list(range(len(self.state_data['timestamps'])))
        # sort index by timestamp
        indices.sort(key=lambda i: self.state_data['timestamps'][i])
        
        # rearrange all data
        sorted_timestamps = [self.state_data['timestamps'][i] for i in indices]
        self.state_data['timestamps'] = sorted_timestamps
        
        for leg_id in range(4):
            for motor_id in range(4):
                for key in ['temp', 'voltage', 'error', 'position', 'speed', 'torque']:
                    sorted_values = [self.state_data['legs'][leg_id]['motors'][motor_id][key][i] for i in indices]
                    self.state_data['legs'][leg_id]['motors'][motor_id][key] = sorted_values
    
    def sort_cmd_data(self):
        """按时间戳排序cmd数据"""
        if not self.cmd_data or not self.cmd_data['timestamps']:
            return
        
        # create index list
        indices = list(range(len(self.cmd_data['timestamps'])))
        # sort index by timestamp
        indices.sort(key=lambda i: self.cmd_data['timestamps'][i])
        
        # rearrange all data
        sorted_timestamps = [self.cmd_data['timestamps'][i] for i in indices]
        self.cmd_data['timestamps'] = sorted_timestamps
        
        for leg_id in range(4):
            sorted_enable = [self.cmd_data['legs'][leg_id]['enable'][i] for i in indices]
            self.cmd_data['legs'][leg_id]['enable'] = sorted_enable
            
            for motor_id in range(4):
                for key in ['target_position', 'target_speed', 'target_torque']:
                    sorted_values = [self.cmd_data['legs'][leg_id]['motors'][motor_id][key][i] for i in indices]
                    self.cmd_data['legs'][leg_id]['motors'][motor_id][key] = sorted_values
    
    def parse_state_log(self, filepath):
        """解析状态日志文件"""
        data = {
            'timestamps': [],
            'legs': [{'motors': [
                {'temp': [], 'voltage': [], 'error': [], 'position': [], 'speed': [], 'torque': []} 
                for _ in range(4)]} for _ in range(4)]
        }
        
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                current_timestamp = None
                line_count = 0
                
                for line in f:
                    line = line.strip()
                    line_count += 1
                    
                    if not line:
                        continue
                    
                    timestamp = self.parse_timestamp(line)
                    if timestamp:
                        current_timestamp = timestamp
                        continue
                    
                    leg_match = re.match(r'leg\[(\d+)\]:\s*(.+)', line)
                    if leg_match and current_timestamp:
                        leg_id = int(leg_match.group(1))
                        
                        try:
                            values = []
                            for x in leg_match.group(2).split(','):
                                x = x.strip()
                                if x:
                                    values.append(float(x))
                            
                            if len(values) >= 24:
                                values = values[:24]
                                if leg_id == 0:
                                    data['timestamps'].append(current_timestamp)
                                
                                for motor_id in range(4):
                                    idx = motor_id * 6
                                    data['legs'][leg_id]['motors'][motor_id]['temp'].append(values[idx])
                                    data['legs'][leg_id]['motors'][motor_id]['voltage'].append(values[idx + 1])
                                    data['legs'][leg_id]['motors'][motor_id]['error'].append(values[idx + 2])
                                    data['legs'][leg_id]['motors'][motor_id]['position'].append(values[idx + 3])
                                    data['legs'][leg_id]['motors'][motor_id]['speed'].append(values[idx + 4])
                                    data['legs'][leg_id]['motors'][motor_id]['torque'].append(values[idx + 5])
                        except ValueError as e:
                            print(f"解析错误 行{line_count}: {e}")
                            continue
                    
                    if line_count % 10000 == 0:
                        self.info_text.insert(tk.END, f"  已处理 {line_count} 行...\n")
                        self.root.update()
                        
        except Exception as e:
            print(f"解析state日志出错: {e}")
            self.info_text.insert(tk.END, f"错误: {e}\n")
            return None
        
        return data if data['timestamps'] else None
    
    def parse_cmd_log(self, filepath):
        """解析命令日志文件"""
        data = {
            'timestamps': [],
            'legs': [{'enable': [], 'motors': [
                {'target_position': [], 'target_speed': [], 'target_torque': []} 
                for _ in range(4)]} for _ in range(4)]
        }
        
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                current_timestamp = None
                line_count = 0
                
                for line in f:
                    line = line.strip()
                    line_count += 1
                    
                    if not line:
                        continue
                    
                    timestamp = self.parse_timestamp(line)
                    if timestamp:
                        current_timestamp = timestamp
                        continue
                    
                    leg_match = re.match(r'leg\[(\d+)\]:\s*(.+)', line)
                    if leg_match and current_timestamp:
                        leg_id = int(leg_match.group(1))
                        
                        try:
                            values = []
                            for x in leg_match.group(2).split(','):
                                x = x.strip()
                                if x:
                                    values.append(float(x))
                            
                            if len(values) >= 21:
                                if leg_id == 0:
                                    data['timestamps'].append(current_timestamp)
                                
                                data['legs'][leg_id]['enable'].append(values[0])
                                
                                for motor_id in range(4):
                                    idx = 1 + motor_id * 5
                                    data['legs'][leg_id]['motors'][motor_id]['target_position'].append(values[idx])
                                    data['legs'][leg_id]['motors'][motor_id]['target_speed'].append(values[idx + 1])
                                    data['legs'][leg_id]['motors'][motor_id]['target_torque'].append(values[idx + 4])
                        except ValueError as e:
                            print(f"解析错误 行{line_count}: {e}")
                            continue
                    
                    if line_count % 10000 == 0:
                        self.info_text.insert(tk.END, f"  已处理 {line_count} 行...\n")
                        self.root.update()
                        
        except Exception as e:
            print(f"解析cmd日志出错: {e}")
            self.info_text.insert(tk.END, f"错误: {e}\n")
            return None
        
        return data if data['timestamps'] else None
    
    def update_plot(self):
        """更新图表显示"""
        if not self.state_data or not self.state_data['timestamps']:
            messagebox.showwarning("警告", "请先加载数据")
            return
        
        # 清除旧图表
        if self.canvas:
            self.canvas.get_tk_widget().destroy()
        if self.toolbar:
            self.toolbar.destroy()
        
        self.info_text.insert(tk.END, "\n正在绘制图表...\n")
        self.root.update()
        
        # 根据视图模式绘制
        mode = self.view_mode_var.get()
        if mode == 'leg':
            self.plot_by_leg()
        else:
            self.plot_by_motor()
    
    def create_time_selector(self):
        """创建时间范围选择器 - 在底部添加交互式时间轴"""
        if not self.full_time_range:
            return
        
        # add reset button area on the left
        from matplotlib.widgets import Button
        
        # button area (left) - move up to display the scale below
        button_ax = self.fig.add_axes([0.01, 0.05, 0.06, 0.025])
        self.reset_button = Button(button_ax, '重置时间', color='lightgray', hovercolor='gray')
        self.reset_button.label.set_fontsize(7)
        self.reset_button.on_clicked(lambda event: self.reset_time_range())
        
        # add time selector on the right (move up to display the scale label below)
        self.time_selector_ax = self.fig.add_axes([0.08, 0.05, 0.90, 0.025])
        
        # draw the time line for the full time range
        full_timestamps = self.state_data['timestamps']
        
        # use the sampled time points to draw the time line (improve performance)
        sample_indices = np.linspace(0, len(full_timestamps)-1, min(1000, len(full_timestamps)), dtype=int)
        sampled_times = [full_timestamps[i] for i in sample_indices]
        
        # draw the time line
        self.time_selector_ax.plot(sampled_times, [0.5]*len(sampled_times), 'b-', alpha=0.3, linewidth=2)
        
        # if there is a selection range, highlight it
        if self.time_range_start and self.time_range_end:
            selected_times = [t for t in sampled_times if self.time_range_start <= t <= self.time_range_end]
            if selected_times:
                self.time_selector_ax.plot(selected_times, [0.5]*len(selected_times), 'r-', alpha=0.7, linewidth=4)
        
        self.time_selector_ax.set_ylim(0, 1)
        self.time_selector_ax.set_yticks([])
        self.time_selector_ax.set_xlabel('时间轴 (拖动选择)', fontsize=7, fontweight='bold')
        self.time_selector_ax.grid(True, alpha=0.3, axis='x')
        
        # time format
        class MillisecondFormatter(mdates.DateFormatter):
            def __init__(self):
                super().__init__('%H:%M:%S.%f')
            
            def __call__(self, x, pos=None):
                result = super().__call__(x, pos)
                if '.' in result:
                    parts = result.split('.')
                    result = parts[0] + '.' + parts[1][:3]
                return result
        
        self.time_selector_ax.xaxis.set_major_formatter(MillisecondFormatter())
        self.time_selector_ax.tick_params(axis='x', rotation=30, labelsize=6, pad=1)
        self.time_selector_ax.xaxis.set_label_position('bottom')
        self.time_selector_ax.xaxis.tick_bottom()
        
        # set the x-axis range to the full time range
        self.time_selector_ax.set_xlim(full_timestamps[0], full_timestamps[-1])
        
        # display the total time range on the time axis
        full_start_str = full_timestamps[0].strftime('%H:%M:%S.%f')[:-3]
        full_end_str = full_timestamps[-1].strftime('%H:%M:%S.%f')[:-3]
        
        # display the total start time on the left
        self.time_selector_ax.text(0.001, 1.5, f'总范围: {full_start_str}', 
                                   transform=self.time_selector_ax.transAxes,
                                   fontsize=6, verticalalignment='bottom', horizontalalignment='left',
                                   bbox=dict(boxstyle='round,pad=0.2', facecolor='lightblue', alpha=0.7))
        
        # display the total end time on the right
        self.time_selector_ax.text(0.999, 1.5, f'{full_end_str}', 
                                   transform=self.time_selector_ax.transAxes,
                                   fontsize=6, verticalalignment='bottom', horizontalalignment='right',
                                   bbox=dict(boxstyle='round,pad=0.2', facecolor='lightblue', alpha=0.7))
        
        # display the selection range annotation on the time axis
        if self.time_range_start and self.time_range_end:
            # if there is a selection range, display the start and end time of the selection range
            start_time_str = self.time_range_start.strftime('%H:%M:%S.%f')[:-3]
            end_time_str = self.time_range_end.strftime('%H:%M:%S.%f')[:-3]
            
            # calculate the relative position of the selection range on the full time axis
            full_start = mdates.date2num(full_timestamps[0])
            full_end = mdates.date2num(full_timestamps[-1])
            sel_start = mdates.date2num(self.time_range_start)
            sel_end = mdates.date2num(self.time_range_end)
            
            # normalize the position (0 to 1)
            start_pos = (sel_start - full_start) / (full_end - full_start) if full_end != full_start else 0
            end_pos = (sel_end - full_start) / (full_end - full_start) if full_end != full_start else 1
            
            # left time annotation (selection range start) - display on the time axis below (data coordinates)
            # use data coordinates to annotate on the time axis
            self.time_selector_ax.annotate(f'◀{start_time_str}', 
                                           xy=(self.time_range_start, 0), 
                                           xytext=(0, -30),
                                           textcoords='offset points',
                                           fontsize=6, 
                                           ha='left',
                                           bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.8),
                                           arrowprops=dict(arrowstyle='->', color='orange', lw=1))
            
            # right time annotation (selection range end) - display on the time axis below (data coordinates)
            self.time_selector_ax.annotate(f'{end_time_str}▶', 
                                           xy=(self.time_range_end, 0), 
                                           xytext=(0, -30),
                                           textcoords='offset points',
                                           fontsize=6, 
                                           ha='right',
                                           bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.8),
                                           arrowprops=dict(arrowstyle='->', color='orange', lw=1))
        
        # add SpanSelector
        def onselect(xmin, xmax):
            try:
                # convert numeric values back to datetime
                self.time_range_start = mdates.num2date(xmin).replace(tzinfo=None)
                self.time_range_end = mdates.num2date(xmax).replace(tzinfo=None)
                
                # update the chart
                self.info_text.insert(tk.END, f"\n⏱ 时间范围: {self.time_range_start.strftime('%H:%M:%S.%f')[:-3]} - {self.time_range_end.strftime('%H:%M:%S.%f')[:-3]}\n")
                self.info_text.see(tk.END)
                self.update_plot()
            except Exception as e:
                print(f"时间选择错误: {e}")
        
        self.span_selector = SpanSelector(
            self.time_selector_ax,
            onselect,
            'horizontal',
            useblit=True,
            props=dict(alpha=0.5, facecolor='red'),
            interactive=True,
            drag_from_anywhere=True,
            button=1  # left click drag
        )
    
    def plot_by_leg(self):
        """按腿部视图绘制 - 显示某一条腿的4个电机"""
        leg_id = self.leg_var.get()
        
        # 点云模式：只显示点云图
        if self.point_cloud_mode:
            self.plot_point_cloud_by_leg(leg_id)
            return
        
        # 常规模式：显示完整的6x4布局
        # create a new chart
        self.fig = Figure(figsize=(14, 16), dpi=90)
        
        # data quantity information
        state_count = len(self.state_data["timestamps"])
        cmd_count = len(self.cmd_data["timestamps"]) if self.cmd_data else 0
        
        title = f'【按腿部视图】Leg {leg_id} - 4个电机对比'
        title += f' | State: {state_count}条'
        if self.cmd_data:
            title += f', Cmd: {cmd_count}条'
        if self.downsample_var.get() and state_count > self.max_display_points:
            title += ' | 已优化显示'
        if self.time_range_start and self.time_range_end:
            title += f' | 时间段: {self.time_range_start.strftime("%H:%M:%S")} - {self.time_range_end.strftime("%H:%M:%S")}'
        self.fig.suptitle(title, fontsize=14, fontweight='bold')
        
        # create 6x4 subplots
        axes = []
        for i in range(6):
            for j in range(4):
                ax = self.fig.add_subplot(6, 4, i * 4 + j + 1)
                axes.append(ax)
        
        state_timestamps = self.state_data['timestamps']
        state_leg = self.state_data['legs'][leg_id]
        
        cmd_timestamps = self.cmd_data['timestamps'] if self.cmd_data else []
        cmd_leg = self.cmd_data['legs'][leg_id] if self.cmd_data else None
        
        for motor_id in range(4):
            state_motor = state_leg['motors'][motor_id]
            cmd_motor = cmd_leg['motors'][motor_id] if cmd_leg else None
            
            # apply time range filtering
            filtered_state = self.filter_data_by_time_range(
                state_timestamps,
                state_motor['position'],
                state_motor['speed'],
                state_motor['torque'],
                state_motor['voltage'],
                state_motor['temp'],
                state_motor['error']
            )
            fstate_t = filtered_state[0]
            fpos, fspd, ftor, fvol, ftmp, ferr = filtered_state[1:]
            
            # downsample data
            pos_t, pos_v = self.downsample_data(fstate_t, fpos)
            spd_t, spd_v = self.downsample_data(fstate_t, fspd)
            tor_t, tor_v = self.downsample_data(fstate_t, ftor)
            vol_t, vol_v = self.downsample_data(fstate_t, fvol)
            tmp_t, tmp_v = self.downsample_data(fstate_t, ftmp)
            
            # position
            ax_idx = motor_id
            axes[ax_idx].plot(pos_t, pos_v, 'b-', linewidth=0.8, alpha=0.8, label='实际位置', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_position'])
                cmd_pos_t, cmd_pos_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_pos_t, cmd_pos_v, 'r--', linewidth=0.8, alpha=0.7, label='目标位置', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 位置 (rad)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('位置 (rad)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # speed
            ax_idx = 4 + motor_id
            axes[ax_idx].plot(spd_t, spd_v, 'g-', linewidth=0.8, alpha=0.8, label='实际速度', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_speed'])
                cmd_spd_t, cmd_spd_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_spd_t, cmd_spd_v, 'r--', linewidth=0.8, alpha=0.7, label='目标速度', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 速度 (rad/s)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('速度 (rad/s)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # torque
            ax_idx = 8 + motor_id
            axes[ax_idx].plot(tor_t, tor_v, 'purple', linewidth=0.8, alpha=0.8, label='实际扭矩', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_torque'])
                cmd_tor_t, cmd_tor_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_tor_t, cmd_tor_v, 'r--', linewidth=0.8, alpha=0.7, label='前馈扭矩', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 扭矩 (Nm)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('扭矩 (Nm)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # voltage
            ax_idx = 12 + motor_id
            axes[ax_idx].plot(vol_t, vol_v, 'cyan', linewidth=0.8, alpha=0.8, rasterized=True)
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 电压 (V)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('电压 (V)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 温度
            ax_idx = 16 + motor_id
            axes[ax_idx].plot(tmp_t, tmp_v, 'orange', linewidth=0.8, alpha=0.8, rasterized=True)
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 温度 (°C)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('温度 (°C)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 错误位
            ax_idx = 20 + motor_id
            err_t, err_v = self.downsample_data(fstate_t, ferr, max_points=2000)
            
            normal_times, normal_values = [], []
            error_01_times, error_01_values = [], []
            large_error_times, large_error_values = [], []
            
            for i, error in enumerate(err_v):
                if error == 1:
                    normal_times.append(err_t[i])
                    normal_values.append(1)
                elif error == 0:
                    error_01_times.append(err_t[i])
                    error_01_values.append(0)
                else:
                    large_error_times.append(err_t[i])
                    large_error_values.append(error)
            
            if len(normal_times) > 100:
                axes[ax_idx].plot(normal_times, normal_values, 'g.', markersize=3, alpha=0.6, label='正常', rasterized=True)
            elif normal_times:
                axes[ax_idx].scatter(normal_times, normal_values, color='green', s=10, alpha=0.6, marker='o', label='正常', zorder=1, rasterized=True)
            
            if len(error_01_times) > 100:
                axes[ax_idx].plot(error_01_times, error_01_values, 'o', color='orange', markersize=4, alpha=0.8, label='失能', rasterized=True)
            elif error_01_times:
                axes[ax_idx].scatter(error_01_times, error_01_values, color='orange', s=15, alpha=0.8, marker='o', label='失能', zorder=2, rasterized=True)
            
            if cmd_leg:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_leg['enable'])
                enable_t, enable_v = self.downsample_data(fcmd[0], fcmd[1], max_points=2000)
                axes[ax_idx].plot(enable_t, enable_v, 'k-', linewidth=1.2, alpha=0.8, label='使能状态', zorder=10, rasterized=True)
            
            axes[ax_idx].set_title(f'Motor {motor_id+1} - 错误位', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('错误状态(0/1)', fontsize=8, color='black')
            axes[ax_idx].set_ylim(-0.1, 1.1)
            axes[ax_idx].tick_params(labelsize=7, axis='y', labelcolor='black')
            axes[ax_idx].legend(fontsize=6, loc='upper left')
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].set_xlabel('时间', fontsize=8)
            
            if large_error_times:
                ax_right = axes[ax_idx].twinx()
                ax_right.scatter(large_error_times, large_error_values, color='red', s=30, alpha=0.9, marker='X', label='错误码', zorder=3, rasterized=True)
                ax_right.set_ylabel('错误码数值', fontsize=8, color='red')
                ax_right.tick_params(axis='y', labelcolor='red', labelsize=7)
                if large_error_values:
                    err_min = min(large_error_values)
                    err_max = max(large_error_values)
                    err_range = err_max - err_min
                    if err_range < 10:
                        err_range = 10
                    ax_right.set_ylim(err_min - err_range*0.1, err_max + err_range*0.1)
                ax_right.legend(fontsize=6, loc='upper right')
            
            class MillisecondFormatter(mdates.DateFormatter):
                def __init__(self):
                    super().__init__('%H:%M:%S.%f')
                def __call__(self, x, pos=None):
                    result = super().__call__(x, pos)
                    if '.' in result:
                        parts = result.split('.')
                        result = parts[0] + '.' + parts[1][:3]
                    return result
            
            axes[ax_idx].xaxis.set_major_formatter(MillisecondFormatter())
            axes[ax_idx].tick_params(axis='x', rotation=45, labelsize=6)
        
        all_timestamps = list(fstate_t)
        if cmd_timestamps:
            fcmd_t = self.filter_data_by_time_range(cmd_timestamps)[0]
            all_timestamps.extend(fcmd_t)
        
        if all_timestamps:
            min_time = min(all_timestamps)
            max_time = max(all_timestamps)
            for ax in axes:
                ax.set_xlim(min_time, max_time)
        
        # 调整布局，为底部时间轴和刻度标签留足够空间
        self.fig.tight_layout(rect=[0, 0.10, 1, 0.97])
        
        # 创建底部时间选择器
        self.create_time_selector()
        
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_container)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
        
        self.toolbar = NavigationToolbar2Tk(self.canvas, self.plot_container)
        self.toolbar.update()
        
        mode_info = ""
        if self.downsample_var.get() and len(self.state_data['timestamps']) > self.max_display_points:
            mode_info = " (已优化)"
        
        self.info_text.insert(tk.END, f"\n✓ 图表已更新 - Leg {leg_id}{mode_info}\n")
        self.info_text.see(tk.END)
    
    def plot_point_cloud_by_leg(self, leg_id):
        """点云模式 - 按腿部视图：只显示4个电机的点云图"""
        if not self.state_data or not self.state_data['timestamps']:
            messagebox.showwarning("警告", "请先加载数据")
            return
        
        # 清除旧图表
        if self.canvas:
            self.canvas.get_tk_widget().destroy()
        if self.toolbar:
            self.toolbar.destroy()
        
        self.info_text.insert(tk.END, "\n正在绘制点云图...\n")
        self.root.update()
        
        # 创建2x2布局的图表
        self.fig = Figure(figsize=(14, 10), dpi=90)
        
        # 标题
        state_count = len(self.state_data["timestamps"])
        title = f'☁️ 点云模式 - Leg {leg_id} 扭矩vs速度分析（全部数据）'
        title += f' | State: {state_count}条'
        if self.time_range_start and self.time_range_end:
            title += f' | 时间段: {self.time_range_start.strftime("%H:%M:%S")} - {self.time_range_end.strftime("%H:%M:%S")}'
        self.fig.suptitle(title, fontsize=14, fontweight='bold')
        
        try:
            # 获取数据
            state_timestamps = self.state_data['timestamps']
            state_leg = self.state_data['legs'][leg_id]
            
            # 创建2x2子图
            for motor_id in range(4):
                ax = self.fig.add_subplot(2, 2, motor_id + 1)
                
                state_motor = state_leg['motors'][motor_id]
                
                # 应用时间范围过滤
                filtered_state = self.filter_data_by_time_range(
                    state_timestamps,
                    state_motor['speed'],
                    state_motor['torque']
                )
                fstate_t, fspd, ftor = filtered_state
                
                # 点云模式：直接使用原始数据，不做任何采样
                if len(fspd) == 0 or len(ftor) == 0:
                    # 数据为空，跳过
                    ax.text(0.5, 0.5, '无数据', ha='center', va='center', fontsize=14)
                    continue
                
                # 绘制点云图（使用全部数据）
                ax.scatter(fspd, ftor, c='blue', s=2, alpha=0.6, rasterized=True)
                ax.set_title(f'Motor {motor_id+1}', fontsize=12, fontweight='bold')
                ax.set_xlabel('速度 (rad/s)', fontsize=10)
                ax.set_ylabel('扭矩 (Nm)', fontsize=10)
                ax.grid(True, alpha=0.3, linestyle='--')
                ax.tick_params(labelsize=9)
                
                # 添加数据点数统计
                ax.text(0.02, 0.98, f'数据点: {len(fspd)}', 
                       transform=ax.transAxes, fontsize=8,
                       verticalalignment='top',
                       bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.7))
            
            # 调整布局
            self.fig.tight_layout(rect=[0, 0.03, 1, 0.97])
            
            # 绘制到画布
            self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_container)
            self.canvas.draw()
            self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
            
            self.toolbar = NavigationToolbar2Tk(self.canvas, self.plot_container)
            self.toolbar.update()
            
            mode_info = " [点云模式 - 全部数据]"
            
            self.info_text.insert(tk.END, f"\n✓ 点云图已更新 - Leg {leg_id}{mode_info}\n")
            self.info_text.see(tk.END)
            
        except Exception as e:
            messagebox.showerror("错误", f"绘制点云图时出错:\n{str(e)}")
            self.info_text.insert(tk.END, f"\n❌ 点云图绘制失败: {e}\n")
            self.info_text.see(tk.END)
            import traceback
            traceback.print_exc()
    
    def plot_point_cloud_by_motor(self, motor_id):
        """点云模式 - 按电机视图：只显示4条腿的点云图"""
        if not self.state_data or not self.state_data['timestamps']:
            messagebox.showwarning("警告", "请先加载数据")
            return
        
        # 清除旧图表
        if self.canvas:
            self.canvas.get_tk_widget().destroy()
        if self.toolbar:
            self.toolbar.destroy()
        
        self.info_text.insert(tk.END, "\n正在绘制点云图...\n")
        self.root.update()
        
        # 创建2x2布局的图表
        self.fig = Figure(figsize=(14, 10), dpi=90)
        
        # 标题
        state_count = len(self.state_data["timestamps"])
        title = f'☁️ 点云模式 - Motor {motor_id+1} 扭矩vs速度分析（全部数据）'
        title += f' | State: {state_count}条'
        if self.time_range_start and self.time_range_end:
            title += f' | 时间段: {self.time_range_start.strftime("%H:%M:%S")} - {self.time_range_end.strftime("%H:%M:%S")}'
        self.fig.suptitle(title, fontsize=14, fontweight='bold')
        
        try:
            # 获取数据
            state_timestamps = self.state_data['timestamps']
            
            # 颜色方案 - 每条腿不同颜色
            leg_colors = ['blue', 'green', 'orange', 'purple']
            leg_names = ['Leg 0', 'Leg 1', 'Leg 2', 'Leg 3']
            
            # 创建2x2子图
            for leg_id in range(4):
                ax = self.fig.add_subplot(2, 2, leg_id + 1)
                
                state_leg = self.state_data['legs'][leg_id]
                state_motor = state_leg['motors'][motor_id]
                
                # 应用时间范围过滤
                filtered_state = self.filter_data_by_time_range(
                    state_timestamps,
                    state_motor['speed'],
                    state_motor['torque']
                )
                fstate_t, fspd, ftor = filtered_state
                
                # 点云模式：直接使用原始数据，不做任何采样
                if len(fspd) == 0 or len(ftor) == 0:
                    # 数据为空，跳过
                    ax.text(0.5, 0.5, '无数据', ha='center', va='center', fontsize=14)
                    continue
                
                # 绘制点云图（使用全部数据）
                color = leg_colors[leg_id]
                ax.scatter(fspd, ftor, c=color, s=2, alpha=0.6, rasterized=True)
                ax.set_title(leg_names[leg_id], fontsize=12, fontweight='bold')
                ax.set_xlabel('速度 (rad/s)', fontsize=10)
                ax.set_ylabel('扭矩 (Nm)', fontsize=10)
                ax.grid(True, alpha=0.3, linestyle='--')
                ax.tick_params(labelsize=9)
                
                # 添加数据点数统计
                ax.text(0.02, 0.98, f'数据点: {len(fspd)}', 
                       transform=ax.transAxes, fontsize=8,
                       verticalalignment='top',
                       bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.7))
            
            # 调整布局
            self.fig.tight_layout(rect=[0, 0.03, 1, 0.97])
            
            # 绘制到画布
            self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_container)
            self.canvas.draw()
            self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
            
            self.toolbar = NavigationToolbar2Tk(self.canvas, self.plot_container)
            self.toolbar.update()
            
            mode_info = " [点云模式 - 全部数据]"
            
            self.info_text.insert(tk.END, f"\n✓ 点云图已更新 - Motor {motor_id+1}{mode_info}\n")
            self.info_text.see(tk.END)
            
        except Exception as e:
            messagebox.showerror("错误", f"绘制点云图时出错:\n{str(e)}")
            self.info_text.insert(tk.END, f"\n❌ 点云图绘制失败: {e}\n")
            self.info_text.see(tk.END)
            import traceback
            traceback.print_exc()
    
    def plot_by_motor(self):
        """按电机视图绘制 - 显示4条腿的某一个电机"""
        motor_id = self.motor_var.get()
        
        # 点云模式：只显示点云图
        if self.point_cloud_mode:
            self.plot_point_cloud_by_motor(motor_id)
            return
        
        # 常规模式：显示完整的6x4布局
        # 创建新图表
        self.fig = Figure(figsize=(14, 16), dpi=90)
        
        # 数据量信息
        state_count = len(self.state_data["timestamps"])
        cmd_count = len(self.cmd_data["timestamps"]) if self.cmd_data else 0
        
        title = f'【按电机视图】Motor {motor_id+1} - 4条腿对比'
        title += f' | State: {state_count}条'
        if self.cmd_data:
            title += f', Cmd: {cmd_count}条'
        if self.downsample_var.get() and state_count > self.max_display_points:
            title += ' | 已优化显示'
        if self.time_range_start and self.time_range_end:
            title += f' | 时间段: {self.time_range_start.strftime("%H:%M:%S")} - {self.time_range_end.strftime("%H:%M:%S")}'
        self.fig.suptitle(title, fontsize=14, fontweight='bold')
        
        # 创建6x4子图
        axes = []
        for i in range(6):
            for j in range(4):
                ax = self.fig.add_subplot(6, 4, i * 4 + j + 1)
                axes.append(ax)
        
        state_timestamps = self.state_data['timestamps']
        cmd_timestamps = self.cmd_data['timestamps'] if self.cmd_data else []
        
        # 颜色方案 - 每条腿不同颜色
        leg_colors = ['blue', 'green', 'orange', 'purple']
        leg_names = ['Leg0', 'Leg1', 'Leg2', 'Leg3']
        
        for leg_id in range(4):
            state_leg = self.state_data['legs'][leg_id]
            cmd_leg = self.cmd_data['legs'][leg_id] if self.cmd_data else None
            
            state_motor = state_leg['motors'][motor_id]
            cmd_motor = cmd_leg['motors'][motor_id] if cmd_leg else None
            
            # 应用时间范围过滤
            filtered_state = self.filter_data_by_time_range(
                state_timestamps,
                state_motor['position'],
                state_motor['speed'],
                state_motor['torque'],
                state_motor['voltage'],
                state_motor['temp'],
                state_motor['error']
            )
            fstate_t = filtered_state[0]
            fpos, fspd, ftor, fvol, ftmp, ferr = filtered_state[1:]
            
            # 下采样数据
            pos_t, pos_v = self.downsample_data(fstate_t, fpos)
            spd_t, spd_v = self.downsample_data(fstate_t, fspd)
            tor_t, tor_v = self.downsample_data(fstate_t, ftor)
            vol_t, vol_v = self.downsample_data(fstate_t, fvol)
            tmp_t, tmp_v = self.downsample_data(fstate_t, ftmp)
            
            color = leg_colors[leg_id]
            
            # 位置
            ax_idx = leg_id
            axes[ax_idx].plot(pos_t, pos_v, color=color, linestyle='-', linewidth=0.8, alpha=0.8, label='实际位置', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_position'])
                cmd_pos_t, cmd_pos_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_pos_t, cmd_pos_v, 'r--', linewidth=0.8, alpha=0.7, label='目标位置', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 位置 (rad)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('位置 (rad)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 速度
            ax_idx = 4 + leg_id
            axes[ax_idx].plot(spd_t, spd_v, color=color, linestyle='-', linewidth=0.8, alpha=0.8, label='实际速度', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_speed'])
                cmd_spd_t, cmd_spd_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_spd_t, cmd_spd_v, 'r--', linewidth=0.8, alpha=0.7, label='目标速度', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 速度 (rad/s)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('速度 (rad/s)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 扭矩
            ax_idx = 8 + leg_id
            axes[ax_idx].plot(tor_t, tor_v, color=color, linestyle='-', linewidth=0.8, alpha=0.8, label='实际扭矩', rasterized=True)
            if cmd_motor:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_motor['target_torque'])
                cmd_tor_t, cmd_tor_v = self.downsample_data(fcmd[0], fcmd[1])
                axes[ax_idx].plot(cmd_tor_t, cmd_tor_v, 'r--', linewidth=0.8, alpha=0.7, label='前馈扭矩', rasterized=True)
                axes[ax_idx].legend(fontsize=7, loc='best')
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 扭矩 (Nm)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('扭矩 (Nm)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 电压
            ax_idx = 12 + leg_id
            axes[ax_idx].plot(vol_t, vol_v, color=color, linestyle='-', linewidth=0.8, alpha=0.8, rasterized=True)
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 电压 (V)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('电压 (V)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 温度
            ax_idx = 16 + leg_id
            axes[ax_idx].plot(tmp_t, tmp_v, color=color, linestyle='-', linewidth=0.8, alpha=0.8, rasterized=True)
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 温度 (°C)', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('温度 (°C)', fontsize=8)
            axes[ax_idx].tick_params(labelsize=7)
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].tick_params(axis='x', labelbottom=False)
            
            # 错误位
            ax_idx = 20 + leg_id
            err_t, err_v = self.downsample_data(fstate_t, ferr, max_points=2000)
            
            normal_times, normal_values = [], []
            error_01_times, error_01_values = [], []
            large_error_times, large_error_values = [], []
            
            for i, error in enumerate(err_v):
                if error == 1:
                    normal_times.append(err_t[i])
                    normal_values.append(1)
                elif error == 0:
                    error_01_times.append(err_t[i])
                    error_01_values.append(0)
                else:
                    large_error_times.append(err_t[i])
                    large_error_values.append(error)
            
            if len(normal_times) > 100:
                axes[ax_idx].plot(normal_times, normal_values, '.', color=color, markersize=3, alpha=0.6, label='正常', rasterized=True)
            elif normal_times:
                axes[ax_idx].scatter(normal_times, normal_values, color=color, s=10, alpha=0.6, marker='o', label='正常', zorder=1, rasterized=True)
            
            if len(error_01_times) > 100:
                axes[ax_idx].plot(error_01_times, error_01_values, 'o', color='orange', markersize=4, alpha=0.8, label='失能', rasterized=True)
            elif error_01_times:
                axes[ax_idx].scatter(error_01_times, error_01_values, color='orange', s=15, alpha=0.8, marker='o', label='失能', zorder=2, rasterized=True)
            
            if cmd_leg:
                fcmd = self.filter_data_by_time_range(cmd_timestamps, cmd_leg['enable'])
                enable_t, enable_v = self.downsample_data(fcmd[0], fcmd[1], max_points=2000)
                axes[ax_idx].plot(enable_t, enable_v, 'k-', linewidth=1.2, alpha=0.8, label='使能状态', zorder=10, rasterized=True)
            
            axes[ax_idx].set_title(f'{leg_names[leg_id]} - 错误位', fontsize=9, fontweight='bold')
            axes[ax_idx].set_ylabel('错误状态(0/1)', fontsize=8, color='black')
            axes[ax_idx].set_ylim(-0.1, 1.1)
            axes[ax_idx].tick_params(labelsize=7, axis='y', labelcolor='black')
            axes[ax_idx].legend(fontsize=6, loc='upper left')
            axes[ax_idx].grid(True, alpha=0.3, linestyle='--')
            axes[ax_idx].set_xlabel('时间', fontsize=8)
            
            if large_error_times:
                ax_right = axes[ax_idx].twinx()
                ax_right.scatter(large_error_times, large_error_values, color='red', s=30, alpha=0.9, marker='X', label='错误码', zorder=3, rasterized=True)
                ax_right.set_ylabel('错误码数值', fontsize=8, color='red')
                ax_right.tick_params(axis='y', labelcolor='red', labelsize=7)
                if large_error_values:
                    err_min = min(large_error_values)
                    err_max = max(large_error_values)
                    err_range = err_max - err_min
                    if err_range < 10:
                        err_range = 10
                    ax_right.set_ylim(err_min - err_range*0.1, err_max + err_range*0.1)
                ax_right.legend(fontsize=6, loc='upper right')
            
            class MillisecondFormatter(mdates.DateFormatter):
                def __init__(self):
                    super().__init__('%H:%M:%S.%f')
                def __call__(self, x, pos=None):
                    result = super().__call__(x, pos)
                    if '.' in result:
                        parts = result.split('.')
                        result = parts[0] + '.' + parts[1][:3]
                    return result
            
            axes[ax_idx].xaxis.set_major_formatter(MillisecondFormatter())
            axes[ax_idx].tick_params(axis='x', rotation=45, labelsize=6)
        
        # 统一x轴范围
        all_timestamps = list(fstate_t)
        if cmd_timestamps:
            fcmd_t = self.filter_data_by_time_range(cmd_timestamps)[0]
            all_timestamps.extend(fcmd_t)
        
        if all_timestamps:
            min_time = min(all_timestamps)
            max_time = max(all_timestamps)
            for ax in axes:
                ax.set_xlim(min_time, max_time)
        
        # adjust layout, leave enough space for the bottom time axis and scale labels
        self.fig.tight_layout(rect=[0, 0.10, 1, 0.97])
        
        # create the bottom time selector
        self.create_time_selector()
        
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_container)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
        
        self.toolbar = NavigationToolbar2Tk(self.canvas, self.plot_container)
        self.toolbar.update()
        
        mode_info = ""
        if self.downsample_var.get() and len(self.state_data['timestamps']) > self.max_display_points:
            mode_info = " (已优化)"
        
        self.info_text.insert(tk.END, f"\n✓ 图表已更新 - Motor {motor_id+1}{mode_info}\n")
        self.info_text.see(tk.END)
    
    def reset_time_range(self):
        """重置时间范围为完整范围"""
        self.time_range_start = None
        self.time_range_end = None
        self.info_text.insert(tk.END, "\n⏱ 时间范围已重置为完整范围\n")
        self.info_text.see(tk.END)
        self.update_plot()
    
    def show_statistics(self):
        """显示统计信息"""
        if not self.state_data:
            messagebox.showinfo("提示", "请先加载数据")
            return
        
        stats = "=== 数据统计 ===\n\n"
        stats += f"State记录数: {len(self.state_data['timestamps'])}\n"
        if self.cmd_data:
            stats += f"Cmd记录数: {len(self.cmd_data['timestamps'])}\n"
        
        if self.full_time_range:
            duration = (self.full_time_range[1] - self.full_time_range[0]).total_seconds()
            stats += f"\n总时长: {duration:.2f}秒\n"
            stats += f"开始时间: {self.full_time_range[0].strftime('%H:%M:%S.%f')[:-3]}\n"
            stats += f"结束时间: {self.full_time_range[1].strftime('%H:%M:%S.%f')[:-3]}\n"
        
        if self.time_range_start and self.time_range_end:
            selected_duration = (self.time_range_end - self.time_range_start).total_seconds()
            stats += f"\n当前选择时长: {selected_duration:.2f}秒\n"
            stats += f"选择开始: {self.time_range_start.strftime('%H:%M:%S.%f')[:-3]}\n"
            stats += f"选择结束: {self.time_range_end.strftime('%H:%M:%S.%f')[:-3]}\n"
        
        messagebox.showinfo("数据统计", stats)

def main():
    """主函数"""
    root = tk.Tk()
    app = MotorLogViewer(root)
    root.mainloop()

if __name__ == "__main__":
    main()
